﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using LNUGame.Controller;

namespace LNUGame.Model
{
    /// <summary>
    /// Controls everything related to the gameplay. Keeps track of and updates the position of the player and enemies.
    /// Checks if the player should gain or lose lives, or if an enemy should die.
    /// </summary>
    class GameModel
    {
        public enum GameState
        {
            PLAYING = 0,
            PLAYER_DEAD = 1,
            FINISHED_LEVEL = 2,
            FINISHED_LAST_LEVEL = 3
        }

        private GameState m_state;

        private Level m_level = new Level();
        private CollisionHandler m_collisonHandler = new CollisionHandler();

        private Player m_player = new Player();
        private List<Enemy> m_enemiesList = new List<Enemy>();
        private List<Healthpack> m_healthpacksList = new List<Healthpack>();

        private bool m_playerHasCollidedWithGround;

        private float m_damageTimer = 0.0f;
        private const float INTERVAL_BETWEEN_DAMAGE = 1.0f;


        /// <summary>
        /// Main constructor. Starts the game as soon as the GameModel is created.
        /// </summary>
        public GameModel()
        {
            StartGame();
        }


        /// <summary>
        /// Gets the enemies and healtpacks from the currently loaded level.
        /// Sets the GameState to PLAYING to allow the controller to take the correct action
        /// </summary>
        private void StartGame()
        {
            m_enemiesList = m_level.Enemies;
            m_healthpacksList = m_level.Healthpacks;
            m_state = GameState.PLAYING;
        }


        /// <summary>
        /// Updats the player and enemy positions.
        /// Updates the timer since the player was last hurt, if it have gone sufficient time we tell the IDamageObserver that the player is able to be damaged again.
        /// Checks if the player is dead or alive, have finished the level, is standing i dangerous terrain, is colliding with an enemy or picking up a healthpack.
        /// </summary>
        /// <param name="a_elapsedTime">How long time since the game was started</param>
        /// <param name="a_soundObserver">An interface allowing the model to influence when a sound should be played</param>
        /// <param name="a_damageObserver">An interface allowing the model to influence when a blood cloud should be shown</param>
        internal void Update(float a_elapsedTime, ISoundObserver a_soundObserver, IDamageObserver a_damageObserver)
        {
            //System.Diagnostics.Debug.WriteLine("");
            UpdatePlayer(a_elapsedTime);
            UpdateEnemies(a_elapsedTime);

            m_damageTimer += a_elapsedTime;

            if (m_damageTimer > INTERVAL_BETWEEN_DAMAGE)
            {
                a_damageObserver.PlayerCanBeHurtAgain();
            }

            CheckIfPlayerIsDead();
            CheckIfPlayerFinishedLevel();
            CheckIfPlayerIsInDangerousTerrain(a_soundObserver, a_damageObserver);
            CheckIfPlayerIsDealingOrTakingDamage(a_soundObserver, a_damageObserver);
            CheckIfPlayerPickedUpHealthpack(a_soundObserver);
        }


        /// <summary>
        /// Updates the position and velocity of the player.
        /// Tries to move the player to the new position, if this can't be done becuase of a collision with the scenery he is moved back to his last position.
        /// </summary>
        /// <param name="a_elapsedTime">How long time since the game was started</param>
        private void UpdatePlayer(float a_elapsedTime)
        {
            Vector2 lastPosition = m_player.Position;
            m_player.Update(a_elapsedTime);
            Vector2 newPosition = m_player.Position;

            m_playerHasCollidedWithGround = false;

            Vector2 velocity = m_player.Velocity;
            Vector2 afterCollidedPos = m_collisonHandler.PlayerCollidWithWorld(m_level, lastPosition, newPosition, m_player.Size, ref velocity, ref m_playerHasCollidedWithGround);

            //set the new velocity and position after collision
            m_player.Position = new Vector2(afterCollidedPos.X, afterCollidedPos.Y);
            m_player.Velocity = new Vector2(velocity.X, velocity.Y);
        }


        /// <summary>
        /// Iterates through every enemy on the level and updates their position and facing.
        /// If an enemy is reaching the end of solid ground he turns around and moves back the way he came.
        /// </summary>
        /// <param name="a_elapsedTime">How long time since the game was started</param>
        private void UpdateEnemies(float a_elapsedTime)
        {
            foreach(Enemy enemy in m_enemiesList)
            {
                if (enemy.CurrentDirection == Enemy.Direction.LEFT)
                {
                    Vector2 enemyPosition = enemy.Position;
                    enemyPosition.X -= enemy.Size.X / 2.0f;
                    if (m_collisonHandler.EnemyIsStandingOverGap(m_level, enemyPosition))
                    {
                        enemy.MoveRight();
                    }
                }
                else if (enemy.CurrentDirection == Enemy.Direction.RIGHT)
                {
                    Vector2 enemyPosition = enemy.Position;
                    enemyPosition.X += enemy.Size.X / 2.0f;
                    if (m_collisonHandler.EnemyIsStandingOverGap(m_level, enemyPosition))
                    {
                        enemy.MoveLeft();
                    }
                }
                enemy.Update(a_elapsedTime);
            }
        }


        /// <summary>
        /// Checks if the player is colliding with an enemy. If he is, we check the positions of the player and enemy the player is colliding with.
        /// If the player is above the enemy, the enemy is removed and a sound is played. If the player is anywhere else, and is currently allowed to be hurt,
        /// the player loses a life, and sound is played, a blood cloud is shown, and the palyer jumps.
        /// </summary>
        /// <param name="a_soundObserver">An interface allowing the model to influence when a sound should be played</param>
        /// <param name="a_damageObserver">An interface allowing the model to influence when a blood cloud should be shown</param>
        internal void CheckIfPlayerIsDealingOrTakingDamage(ISoundObserver a_soundObserver, IDamageObserver a_damageObserver)
        {
            // Can't use foreach for some reason?
            for (int i = 0; i < m_enemiesList.Count; i++)
            {
                if (m_collisonHandler.CollisionBetweenSprites(m_player, m_enemiesList[i]))
                {
                    if (m_player.Position.Y - m_player.Size.Y < m_enemiesList[i].Position.Y - m_enemiesList[i].Size.Y)
                    {
                        a_soundObserver.EnemyKilled();
                        m_enemiesList.Remove(m_enemiesList[i]);
                    }

                    //if (m_player.Position.X + m_player.Size.X / 2 > m_enemiesList[i].Position.X - m_enemiesList[i].Size.X / 2 &&
                    //    m_player.Position.X - m_player.Size.X / 2 < m_enemiesList[i].Position.X + m_enemiesList[i].Size.X / 2)
                    //{
                    //    if (m_player.Position.Y - m_player.Size.Y < m_enemiesList[i].Position.Y &&
                    //        m_player.Position.Y > m_enemiesList[i].Position.Y - m_enemiesList[i].Size.Y)
                    //    {
                    //        a_soundObserver.EnemyKilled();
                    //        m_enemiesList.Remove(m_enemiesList[i]);
                    //    }
                    //}
                    else if (CanBeHurt())
                    {
                        a_soundObserver.PainFromEnemy();
                        a_damageObserver.PlayerIsHurtAt(m_player.Position);
                        m_player.RemoveLife();
                        m_player.Jump();
                    }
                }
            }
        }


        /// <summary>
        /// Checks if the player is standing i dangerous terrain. If he is a sound is played, a blood cloud is drawn and the player loses a life.
        /// </summary>
        /// <param name="a_soundObserver">An interface allowing the model to influence when a sound should be played</param>
        /// <param name="a_damageObserver">An interface allowing the model to influence when a blood cloud should be shown</param>
        private void CheckIfPlayerIsInDangerousTerrain(ISoundObserver a_soundObserver, IDamageObserver a_damageObserver)
        {
            if (m_level.IsStandingInSpikes(m_player.Position, m_player.Size))
            {
                if (CanBeHurt())
                {
                    a_soundObserver.PainFromSpikes();
                    a_damageObserver.PlayerIsHurtAt(m_player.Position);
                    m_player.RemoveLife();
                }
            }
        }


        /// <summary>
        /// Checks if it have gone sufficient time from since the player was last hurt to allow us to hurt him again.
        /// </summary>
        /// <returns>True if the player can currently be hurt, false otherwise</returns>
        private bool CanBeHurt()
        {
            if (m_damageTimer > INTERVAL_BETWEEN_DAMAGE)
            {
                m_damageTimer = 0.0f;
                return true;
            }
            return false;
        }


        /// <summary>
        /// Iterates through every healthpack on the level and checks if the player collides (i.e. picks up) one of them.
        /// When this happens the healthpack i removed from the level, the player is given a extra life, and a sound is played.
        /// </summary>
        /// <param name="a_soundObserver">An interface allowing the model to influence when a sound should be played</param>
        internal void CheckIfPlayerPickedUpHealthpack(ISoundObserver a_soundObserver)
        {
            // Can't use foreach for some reason?
            for(int i = 0 ; i < m_healthpacksList.Count ; i++)
            {
                if (m_collisonHandler.CollisionBetweenSprites(m_player, m_healthpacksList[i]))
                {
                    a_soundObserver.PickupPill();
                    m_player.AddLife();
                    m_healthpacksList.Remove(m_healthpacksList[i]);
                }
            }
        }


        /// <summary>
        /// Checks if the player is dead, i.e. have 0 or less lives.
        /// If he is, sets the GameState to PLAYER_DEAD to allow the controller to take the correct action
        /// </summary>
        private void CheckIfPlayerIsDead()
        {
            if (m_player.RemainingLives <= 0)
            {
                m_state = GameState.PLAYER_DEAD;
            }
        }


        /// <summary>
        /// Queries the currently loaded level to see if the player have finished the level.
        /// If he have, the GameState is changed to FINISHED_LEVEL to allow the controller to take the correct action.
        /// </summary>
        private void CheckIfPlayerFinishedLevel()
        {
            if (m_level.IsFinishingLevel(m_player.Position, m_player.Size))
            {
                m_state = GameState.FINISHED_LEVEL;
            }
        }


        /// <summary>
        /// Moves the player to the right by calling MoveRight in the Player class
        /// </summary>
        internal void MoveRight()
        {
            m_player.MoveRight();
        }


        /// <summary>
        /// Moves the player to the left by calling MoveLeft in the Player class
        /// </summary>
        internal void MoveLeft()
        {
            m_player.MoveLeft();
        }


        /// <summary>
        /// Makes the player jump by calling Jump in the Player class
        /// </summary>
        internal void DoJump()
        {
            if (m_playerHasCollidedWithGround)
            {
                m_player.Jump();
            }
        }


        /// <summary>
        /// Preperas for and loads the next level by resetting the player position, increasing the level number, loading the level and starting the game anew.
        /// </summary>
        internal void LoadNextLevel()
        {
            m_player.ResetPosition();
            m_level.CurrentLevel++;
            m_level.LoadLevel();
            StartGame();
        }


        /// <summary>
        /// Returns the current player position
        /// </summary>
        /// <returns>A Vector2 containing the current position of the player</returns>
        internal Vector2 GetPlayerPosition()
        {
            return m_player.Position;
        }


        /// <summary>
        /// Returns the currently loaded level
        /// </summary>
        /// <returns>A Level object containing the currently loaded level</returns>
        internal Level GetLevel()
        {
            return m_level;
        }


        /// <summary>
        /// Returns the number of remaining lives the player have
        /// </summary>
        /// <returns>An int detailing the number of remaining lives the player have</returns>
        internal int GetPlayerRemainingLives()
        {
            return m_player.RemainingLives;
        }


        /// <summary>
        /// Returns which direction the player is moving in
        /// </summary>
        /// <returns>A Direction showing which way the player is turned</returns>
        internal Player.Direction GetPlayerDirection()
        {
            return m_player.CurrentDirection;
        }


        /// <summary>
        /// Returns the GameState
        /// </summary>
        /// <returns>The current GameState</returns>
        internal GameState GetGameState()
        {
            return m_state;
        }


        /// <summary>
        /// Iterates through all enemies on the level and gets their position. These position are stored in a List and then returned.
        /// </summary>
        /// <returns>A List containing the current positions of all enemies on the level</returns>
        internal List<Vector2> GetEnemyPositions()
        {
            List<Vector2> enemyPositions = new List<Vector2>(m_enemiesList.Count);
            foreach(Enemy enemy in m_enemiesList)
            {
                enemyPositions.Add(enemy.Position);
            }

            return enemyPositions;
        }


        /// <summary>
        /// Iterates through all enemies on the level and gets which way they are currently facing. These facings are stored in a List and then returned.
        /// </summary>
        /// <returns>A List containing the current facing of all enemies on the level</returns>
        internal List<Enemy.Direction> GetEnemyDirections()
        {
            List<Enemy.Direction> enemyDirections = new List<Enemy.Direction>(m_enemiesList.Count);
            foreach(Enemy enemy in m_enemiesList)
            {
                enemyDirections.Add(enemy.CurrentDirection);
            }
            return enemyDirections;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="a_state"></param>
        internal void SetGameState(GameModel.GameState a_state)
        {
            m_state = a_state;
        }


        /// <summary>
        /// Iterates through all healthpacks on the level and gets their position. These position are stored in a List and then returned.
        /// </summary>
        /// <returns>A List containing the positions of all healtpacks on the level</returns>
        internal List<Vector2> GetHealthpackPositions()
        {
            List<Vector2> healthpackPositions = new List<Vector2>(m_healthpacksList.Count);
            foreach(Healthpack healthpack in m_healthpacksList)
            {
                healthpackPositions.Add(healthpack.Position);
            }

            return healthpackPositions;
        }
    }
}
